# 1.3 第一个 Agent

> **Week 1 | Lesson 1.3 | 45 分钟**

---

## 学习目标

完成本课后，你将能够：

- 构建一个能搜索并回答问题的 AI Agent
- 理解 Agent 工具（Tools）的注册和调用方式
- 使用 OpenAI SDK 或 Ollama 运行你的 Agent
- 修改 Agent 的行为，让它回答不同类型的问题
- 读懂 Agent 的完整运行输出

---

## 1. 项目结构

在开始之前，确保你已经完成了 1.2 的环境配置。我们继续使用之前创建的项目：

```
agent-hello/
├── .env              # API Key
├── pyproject.toml    # 项目依赖
├── uv.lock           # 依赖锁定文件
└── first_agent.py    # 本节的主角
```

---

## 2. 第一步：最简单的 Agent

我们先从最基础的版本开始——一个能调用搜索工具回答问题的 Agent。

```python
# first_agent.py
"""
第一个 AI Agent：能搜索并回答问题
"""
from openai import OpenAI
from dotenv import load_dotenv
import os
import json

# 加载环境变量
load_dotenv()

# 判断使用 OpenAI 还是 Ollama
api_key = os.getenv("OPENAI_API_KEY", "")

if api_key == "ollama":
    client = OpenAI(
        base_url="http://localhost:11434/v1",
        api_key="ollama",
    )
    MODEL = "qwen2.5:7b"
else:
    client = OpenAI(api_key=api_key)
    MODEL = "gpt-4o-mini"


def search_knowledge(query: str) -> str:
    """
    工具函数：模拟搜索引擎
    在实际应用中，这里可以替换为真实的搜索 API
    如 Tavily、Google Search API 等
    """
    # 模拟的知识库
    knowledge = {
        "Python": (
            "Python 是一种高级编程语言，由 Guido van Rossum 于 1991 年发布。"
            "它以代码可读性著称，使用缩进来定义代码块。"
            "Python 广泛应用于 Web 开发、数据分析、人工智能、自动化等领域。"
            "最新版本是 Python 3.12，引入了性能改进和新语法特性。"
        ),
        "AI": (
            "人工智能（Artificial Intelligence, AI）是让机器模拟人类智能的技术。"
            "包括机器学习、深度学习、自然语言处理、计算机视觉等方向。"
            "2023-2024 年大语言模型（LLM）的爆发让 AI 进入了新阶段。"
        ),
        "Agent": (
            "AI Agent 是能自主使用工具完成复杂任务的智能系统。"
            "核心组件包括：大语言模型（LLM）、记忆系统、工具调用能力、"
            "以及自主决策机制。Agent 可以搜索信息、操作软件、执行代码等。"
        ),
        "HTTP": (
            "HTTP（HyperText Transfer Protocol）是超文本传输协议。"
            "它是 Web 通信的基础，定义了浏览器和服务器之间的请求-响应模式。"
            "常用状态码：200（成功）、404（未找到）、500（服务器错误）。"
        ),
        "TCP": (
            "TCP（Transmission Control Protocol）是传输控制协议。"
            "它提供可靠的、面向连接的通信服务，保证数据按序、无差错到达。"
            "HTTP 基于 TCP 协议运行。三次握手建立连接，四次挥手断开连接。"
        ),
    }

    # 简单的关键词匹配
    for key, value in knowledge.items():
        if key.lower() in query.lower():
            return value

    return f"抱歉，我的知识库中没有关于 '{query}' 的信息。"


# ========== 工具描述（告诉 Agent 这个工具是干什么的） ==========
search_tool = {
    "type": "function",
    "function": {
        "name": "search_knowledge",
        "description": "搜索知识库，回答关于某个主题的问题",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "要搜索的关键词",
                }
            },
            "required": ["query"],
        },
    },
}


def run_agent(user_question: str) -> str:
    """
    Agent 主函数：接收问题 -> 决定是否搜索 -> 给出回答

    工作流程：
    1. 将用户问题发给 LLM，并告知它有一个 search_knowledge 工具可用
    2. LLM 决定是否需要调用工具
    3. 如果需要，执行工具获取结果
    4. 将工具结果发回 LLM，生成最终回答
    """
    print(f"\n{'='*50}")
    print(f"用户: {user_question}")
    print(f"{'='*50}")

    # 第一步：发送问题给 LLM，附带工具描述
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": "你是一个知识渊博的助手。当用户提问时，"
             "如果问题涉及你不知道的信息，请使用 search_knowledge 工具搜索后再回答。"},
            {"role": "user", "content": user_question},
        ],
        tools=[search_tool],  # 告诉 LLM 有哪些工具可用
        tool_choice="auto",   # 让 LLM 自主决定是否需要工具
    )

    # 检查 LLM 是否决定调用工具
    message = response.choices[0].message

    if message.tool_calls:
        # --- LLM 决定使用工具 ---
        for tool_call in message.tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            print(f"[Agent] 决定调用工具: {function_name}")
            print(f"[Agent] 工具参数: {function_args}")

            # 执行工具
            if function_name == "search_knowledge":
                tool_result = search_knowledge(**function_args)

            print(f"[Agent] 工具返回: {tool_result[:80]}...")

            # 第二步：将工具结果发回 LLM
            final_response = client.chat.completions.create(
                model=MODEL,
                messages=[
                    {"role": "system", "content": "你是一个知识渊博的助手。"
                     "根据工具搜索的结果，用简洁的中文回答用户问题。"},
                    {"role": "user", "content": user_question},
                    {"role": "assistant", "tool_calls": [tool_call]},
                    {"role": "tool", "tool_call_id": tool_call.id, "content": tool_result},
                ],
            )

            answer = final_response.choices[0].message.content
    else:
        # --- LLM 认为不需要工具，直接回答 ---
        print(f"[Agent] 直接回答（不需要工具）")
        answer = message.content

    print(f"\nAgent: {answer}")
    return answer


# ========== 运行 Agent ==========
if __name__ == "__main__":
    print("第一个 AI Agent — 搜索问答")
    print("输入 'quit' 退出\n")

    while True:
        question = input("你: ").strip()
        if question.lower() in ("quit", "exit", "退出"):
            print("再见！")
            break
        if not question:
            continue

        try:
            run_agent(question)
            print()
        except Exception as e:
            print(f"出错了: {e}")
            print("提示：请检查 .env 中的 API Key 是否正确")
```

---

## 3. 运行你的 Agent

```bash
# 使用 OpenAI API
uv run python first_agent.py

# 使用 Ollama（需要先设置 .env 中 OPENAI_API_KEY=ollama 并启动 Ollama）
ollama serve  # 在另一个终端运行
uv run python first_agent.py
```

### 完整运行输出示例

```
第一个 AI Agent — 搜索问答
输入 'quit' 退出

==================================================
用户: 什么是 Python？
==================================================
[Agent] 决定调用工具: search_knowledge
[Agent] 工具参数: {'query': 'Python'}
[Agent] 工具返回: Python 是一种高级编程语言，由 Guido van Rossum 于 1991 年发布。它以代码...

Agent: Python 是一种由 Guido van Rossum 于 1991 年发布的高级编程语言，以代码可读性著称。
它广泛应用于 Web 开发、数据分析、人工智能和自动化等领域。最新版本是 Python 3.12。

你: HTTP 是什么？
==================================================
[Agent] 决定调用工具: search_knowledge
[Agent] 工具参数: {'query': 'HTTP'}
[Agent] 工具返回: HTTP（HyperText Transfer Protocol）是超文本传输协议。它是 Web 通信的基础...

Agent: HTTP（超文本传输协议）是 Web 通信的基础协议，定义了浏览器和服务器之间的请求-响应模式。
常见的状态码有 200（成功）、404（未找到）和 500（服务器错误）。

你: 今天天气怎么样？
==================================================
[Agent] 直接回答（不需要工具）

Agent: 我无法获取实时天气信息，因为我的知识库中没有天气数据。
建议你查看手机上的天气应用或访问天气预报网站获取最新信息。

你: quit
再见！
```

注意观察：
- 当问题在我们的知识库里时，Agent **调用工具搜索**后回答
- 当问题是天气等实时信息时，Agent **判断工具无法解决**，直接说明情况

---

## 4. 代码拆解：每一部分的作用

### 4.1 客户端初始化

```python
client = OpenAI(api_key=api_key)
```

这是与 LLM 通信的桥梁。所有后续的对话请求都通过它发送。

### 4.2 工具定义

```python
search_tool = {
    "type": "function",
    "function": {
        "name": "search_knowledge",          # 工具名称
        "description": "搜索知识库...",       # 告诉 LLM 这个工具干什么
        "parameters": {                       # 工具需要的参数
            "query": {"type": "string", "description": "要搜索的关键词"}
        },
        "required": ["query"]                # 必须提供的参数
    },
}
```

**工具定义的核心是 `description`**——它告诉 LLM 什么时候该用这个工具。描述越准确，LLM 越能正确使用。

### 4.3 工具调用流程

```
用户问题
  │
  ▼
┌──────────────────────────────────────────┐
│  发给 LLM，附带工具描述                    │
│  LLM 决定：需要工具 or 直接回答？           │
└──────────────────┬───────────────────────┘
                   │
         ┌─────────┴─────────┐
         ▼                   ▼
    需要工具              直接回答
         │                   │
         ▼                   ▼
  执行工具函数          返回 LLM 的回答
         │
         ▼
  将工具结果发回 LLM
         │
         ▼
  LLM 整合结果，生成回答
         │
         ▼
    返回最终答案
```

---

## 5. 进阶：添加更多工具

一个真正的 Agent 应该有多个工具。我们来添加一个计算器工具。

```python
# multi_tool_agent.py
"""多工具 Agent：搜索 + 计算"""
from openai import OpenAI
from dotenv import load_dotenv
import os
import json
import math

load_dotenv()

api_key = os.getenv("OPENAI_API_KEY", "")
if api_key == "ollama":
    client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
    MODEL = "qwen2.5:7b"
else:
    client = OpenAI(api_key=api_key)
    MODEL = "gpt-4o-mini"


def search_knowledge(query: str) -> str:
    """搜索知识库"""
    knowledge = {
        "圆": "圆的面积公式是 πr²，周长公式是 2πr。其中 r 是半径，π ≈ 3.14159。",
        "速度": "速度 = 距离 / 时间。单位通常是 m/s 或 km/h。",
    }
    for key, value in knowledge.items():
        if key in query:
            return value
    return f"未找到关于 '{query}' 的信息。"


def calculator(expression: str) -> str:
    """
    安全的计算器工具
    只允许数学运算，不执行任意代码
    """
    # 只允许数字和基础运算符
    allowed = set("0123456789+-*/(). ")
    if not all(c in allowed for c in expression):
        return "错误：表达式包含不允许的字符，只允许数字和 +、-、*、/、()。"

    try:
        result = eval(expression)  # 在受限环境下安全
        return f"计算结果: {expression} = {result}"
    except Exception as e:
        return f"计算错误: {e}"


# 两个工具的描述
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_knowledge",
            "description": "搜索知识库，获取某个概念的信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"}
                },
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "执行数学计算，如加减乘除",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "数学表达式，如 3.14 * 5 * 5"}
                },
                "required": ["expression"],
            },
        },
    },
]

# 工具名称到函数的映射
AVAILABLE_FUNCTIONS = {
    "search_knowledge": search_knowledge,
    "calculator": calculator,
}


def run_agent(question: str) -> str:
    """运行 Agent，支持多轮工具调用"""
    print(f"\n用户: {question}")

    messages = [
        {"role": "system", "content": "你是一个有用的助手。回答问题时可以"
         "使用 search_knowledge 搜索知识，或使用 calculator 进行数学计算。"},
        {"role": "user", "content": question},
    ]

    response = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )

    message = response.choices[0].message

    if message.tool_calls:
        messages.append(message)  # 把 LLM 的工具调用请求加入消息历史

        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)

            print(f"  → 调用工具: {func_name}({func_args})")

            # 执行对应的工具函数
            func = AVAILABLE_FUNCTIONS[func_name]
            result = func(**func_args)

            print(f"  → 结果: {result[:80]}...")

            # 添加工具结果到消息历史
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

        # 将工具结果发回 LLM，生成最终回答
        final = client.chat.completions.create(
            model=MODEL,
            messages=messages,
        )
        answer = final.choices[0].message.content
    else:
        answer = message.content

    print(f"Agent: {answer}")
    return answer


if __name__ == "__main__":
    print("多工具 Agent — 搜索 + 计算")
    print("输入 'quit' 退出\n")

    # 预设测试问题
    test_questions = [
        "圆的面积公式是什么？",
        "半径为 5 的圆，面积是多少？",          # 需要搜索 + 计算
        "1234 * 5678 等于多少？",              # 只需计算
        "Python 是什么？",                      # 不在知识库里
    ]

    for q in test_questions:
        run_agent(q)
        print("-" * 40)
```

**运行输出：**

```
多工具 Agent — 搜索 + 计算

用户: 圆的面积公式是什么？
  → 调用工具: search_knowledge({'query': '圆的面积公式'})
  → 结果: 圆的面积公式是 πr²，周长公式是 2πr。其中 r 是半径，π ≈ 3.14159。...
Agent: 圆的面积公式是 πr²（pi 乘以半径的平方），周长公式是 2πr。

----------------------------------------
用户: 半径为 5 的圆，面积是多少？
  → 调用工具: search_knowledge({'query': '圆'})
  → 结果: 圆的面积公式是 πr²，周长公式是 2πr。其中 r 是半径，π ≈ 3.14159。...
  → 调用工具: calculator({'expression': '3.14159 * 5 * 5'})
  → 结果: 计算结果: 3.14159 * 5 * 5 = 78.53975...
Agent: 半径为 5 的圆面积约为 78.54 平方单位（使用 π ≈ 3.14159）。

----------------------------------------
用户: 1234 * 5678 等于多少？
  → 调用工具: calculator({'expression': '1234 * 5678'})
  → 结果: 计算结果: 1234 * 5678 = 7006652...
Agent: 1234 × 5678 = 7,006,652

----------------------------------------
用户: Python 是什么？
Agent: 抱歉，我的知识库中没有关于 'Python 是什么？' 的信息。
```

关键观察：对于"半径为 5 的圆面积"这个问题，Agent **先后调用了两个工具**——先搜索圆的公式，再计算结果。这就是 Agent 的自主决策能力。

---

## 6. 动手练习

### 练习 1：添加天气查询工具

在 `multi_tool_agent.py` 的基础上，添加一个 `get_weather` 工具。由于我们没有真实的天气 API，可以模拟返回数据：

```python
def get_weather(city: str) -> str:
    """模拟天气查询"""
    weather_data = {
        "北京": "晴，22°C，湿度 30%",
        "上海": "多云，25°C，湿度 60%",
        "深圳": "小雨，28°C，湿度 80%",
    }
    return weather_data.get(city, f"暂无 {city} 的天气数据")
```

记得同时：
1. 在 `tools` 列表中添加这个工具的描述
2. 在 `AVAILABLE_FUNCTIONS` 中添加映射

### 练习 2：让 Agent 支持多轮对话

修改 `run_agent` 函数，使其能记住之前的对话内容。例如：

```
用户: 圆的面积公式是什么？
Agent: 圆的面积公式是 πr²...
用户: 那半径是 3 呢？    ← Agent 应该知道"那"指的是圆的面积
Agent: 半径为 3 的圆面积约为 28.27...
```

提示：维护一个 `conversation_history` 列表。

### 练习 3：切换到 Ollama 运行

1. 修改 `.env` 文件：`OPENAI_API_KEY=ollama`
2. 在另一个终端启动 Ollama：`ollama serve`
3. 运行同一个脚本，观察结果差异

---

## 本节总结

- 一个 Agent 的核心流程：**接收问题 → 决定是否用工具 → 调用工具 → 整合回答**
- **工具描述（description）** 是 Agent 能否正确使用工具的关键——它教 LLM 什么时候用什么工具
- Agent 可以调用**多个工具**，甚至可以**链式调用**（一个工具的结果作为另一个工具的输入）
- `tool_choice="auto"` 让 LLM 自行决定是否需要工具，这是 Agent 自主决策的体现
- 同一个代码框架可以同时支持 **OpenAI API** 和 **Ollama 本地模型**，只需切换配置

下一课，我们将深入学习 Prompt 工程——如何让 Agent 的输出更准确、更可控。
